解決資料庫與數據文件在容器重啟後的丟失問題。
在 Kubernetes 中,Pod 的生命週期是短暫的。如果把 Wafer BI 的 Delta Lake 大數據直接寫在 Python 容器的檔案系統裡,當 K8S 因為負載過高或節點更新把 Pod 砍掉重建時——恭喜,數千萬筆晶圓測試資料瞬間蒸發,一位元組都不剩。
Day 8 我們才炫耀過「刪 Pod 兩秒自動復活」,但復活的是全新的 Pod,裡面的資料可不會跟著轉生。
K8S 用兩個資源解決這件事:
應用只管開請款單,至於硬碟從哪來,交給 StorageClass 動態配置。這個抽象讓同一份 YAML 在不同環境都能跑而不用改應用——本機 Docker Desktop 預設是 hostpath,換成雲端託管的 CSI 驅動(例如 OCI 的 oci-bv)也一樣,換的只是 StorageClass,PVC 宣告本身不用動。
除了「要多大空間」,PVC 請款單上還有一欄容易被忽略:accessModes——宣告這塊空間允許怎麼被掛載。K8S 定義了三種:
hostPath/local-path 完全不支援這個模式這個專案實際用哪一種? 目前 Helm Chart 裡真的有掛 PVC 的,是 postgres 跟 ai-mcp-service(存放向量資料庫 ChromaDB 的資料),兩者都宣告 accessModes: [ReadWriteOnce],也都沒有指定 storageClassName,直接吃叢集預設值。選 RWO 的原因很單純:這兩個服務都只開 1 個副本,是典型的單體 stateful 服務,沒有「好幾個 Pod 同時要讀寫同一份資料」的需求,RWO 已經夠用;反而是硬要上 RWX,會是不必要的過度設計(下面會解釋為什麼)。
跟 Docker 的 bind mount 是不是同一件事? 概念上有關聯,但不是同一層抽象。Docker 的 bind mount(-v /host/path:/container/path)是直接把「主機上的某個路徑」掛進容器,沒有任何中間層——容器因此綁死那台特定主機的檔案系統佈局,換一台機器路徑不對就掛不起來。K8S 的 PV/PVC 反過來設計:應用先宣告「我要多少空間、要什麼讀寫模式」(PVC),至於用什麼東西去滿足這個請求,交給 StorageClass 動態決定(PV)。另外,K8S 的 hostPath PV 類型,底層做的事情其實跟 Docker bind mount 一模一樣(把 Node 上某個路徑掛進 Pod),只是外面包了一層 PVC 的可攜式介面——所以本機 Docker Desktop 用的 hostpath StorageClass,說穿了就是「K8S 化的 bind mount」,差別在於應用程式的 YAML 完全不知道底層是 bind mount,只認得那張「請款單」。
兩個 Pod 共用一份 Volume,要怎麼避免同時寫入互相搞壞? 這裡有幾層要拆開看:
ReadWriteOnce 是「只有一個 Pod 能掛」,官方定義其實是「只有一個 Node 能以讀寫模式掛載」——如果兩個 Pod 剛好被排程到同一台 Node 上,K8S 並不會阻止它們同時掛上同一個 RWO Volume。hostPath/local-path 完全不支援。flock/fcntl 之類的 advisory lock,但要注意 NFS 上的鎖行為不一定可靠,得看後端實作_delta_log/ 用原子性的 commit 檔案協調寫入,就算真的有多個行程動到同一份資料,靠的也是這層交易日誌本身,而不是期待底層檔案系統幫你處理併發對 postgres 這種傳統關聯式資料庫,答案其實更直接:不要讓兩個 Postgres 行程指向同一份資料目錄。這不是「怎麼避免衝突」的問題,是 Postgres 從設計上就不支援多行程共寫同一份資料檔案,兩個實例搶同一個 data directory 只會直接把資料庫弄壞。這也是為什麼 postgres-pvc 就是單純的 RWO + 單一副本,從一開始就沒有考慮 RWX 這條路。
建立 PVC,並掛載到容器的 /app/wafer_delta_table:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wafer-delta-pvc
namespace: k8sdemo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # 本機示範用 1Gi 就夠
containers:
- name: backend
image: ghcr.io/darkschneider1024/wafer-bi-backend:latest
volumeMounts:
- name: delta-storage
mountPath: /app/wafer_delta_table
volumes:
- name: delta-storage
persistentVolumeClaim:
claimName: wafer-delta-pvc
口說無憑,直接做給你看。流程:掛載 PVC → 生成 6.8MB 的 Delta Lake 資料 → 砍掉 Pod → 到新 Pod 裡看資料還在不在:

▲ PVC 持久化實測:刪除 Pod 後,新 Pod 裡的 Delta Lake 資料一位元組都沒少
重點看最後一段:Pod 名字從 8r2j2 換成了 4b5g5(貨真價實的新 Pod),但 /app/wafer_delta_table 裡的 _delta_log 和 parquet 檔完好如初,du 還是那個 6.8M。
一個小細節:PVC 掛上去的瞬間,掛載點會遮蔽 (shadow) image 裡原本烘進去的資料,所以第一次啟動時目錄是空的,要重新生成或搬移資料進去。這個行為跟 Linux 的 mount 一模一樣,第一次遇到很容易愣住(我就愣了幾分鐘,還以為資料被吃了 哈哈)。
現在就算 Python Pod 被 K8S 重啟一百次,晶圓數據也安然無恙。但還有一類東西不能寫死在 image 裡——資料庫密碼、JWT 密鑰、環境設定。明天來聊 ConfigMap 與 Secret 的安全實踐。